/*
 * Copyright 2015 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.tom_roush.pdfbox.pdmodel.common;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.tom_roush.pdfbox.cos.COSArray;
import com.tom_roush.pdfbox.cos.COSBase;
import com.tom_roush.pdfbox.pdmodel.PDDocument;
import com.tom_roush.pdfbox.pdmodel.PDPage;
import com.tom_roush.pdfbox.pdmodel.interactive.annotation.AnnotationFilter;
import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationSquareCircle;
import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationTextMarkup;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class COSArrayListTest {

   @Rule
   public ExpectedException thrown = ExpectedException.none();

   // next two entries are to be used for comparison with
   // COSArrayList behaviour in order to ensure that the
   // intended object is now at the correct position.
   // Will also be used for Collection/Array based setting
   // and comparison
   static List<PDAnnotation> tbcAnnotationsList;
   static COSBase[] tbcAnnotationsArray;

   // next entries are to be used within COSArrayList
   static List<PDAnnotation> annotationsList;
   static COSArray annotationsArray;

   // to be used when testing retrieving filtered items as can be done with
   // {@link PDPage.getAnnotations(AnnotationFilter annotationFilter)}
   static PDPage pdPage;

   private static final File OUT_DIR = new File("target/test-output/pdmodel/common");

   /*
    * Create three new different annotations and add them to the Java List/Array as
    * well as PDFBox List/Array implementations.
    */
   @Before
   public void setUp() throws Exception {
      annotationsList = new ArrayList<PDAnnotation>();
      PDAnnotationTextMarkup txtMark = new PDAnnotationTextMarkup(PDAnnotationTextMarkup.SUB_TYPE_HIGHLIGHT);
      PDAnnotationLink txtLink = new PDAnnotationLink();
      PDAnnotationSquareCircle aCircle = new PDAnnotationSquareCircle(PDAnnotationSquareCircle.SUB_TYPE_CIRCLE);

      annotationsList.add(txtMark);
      annotationsList.add(txtLink);
      annotationsList.add(aCircle);
      annotationsList.add(txtLink);
      assertEquals(4, annotationsList.size());

      tbcAnnotationsList = new ArrayList<PDAnnotation>();
      tbcAnnotationsList.add(txtMark);
      tbcAnnotationsList.add(txtLink);
      tbcAnnotationsList.add(aCircle);
      tbcAnnotationsList.add(txtLink);
      assertEquals(4, tbcAnnotationsList.size());

      annotationsArray = new COSArray();
      annotationsArray.add(txtMark);
      annotationsArray.add(txtLink);
      annotationsArray.add(aCircle);
      annotationsArray.add(txtLink);
      assertEquals(4, annotationsArray.size());

      tbcAnnotationsArray = new COSBase[4];
      tbcAnnotationsArray[0] = txtMark.getCOSObject();
      tbcAnnotationsArray[1] = txtLink.getCOSObject();
      tbcAnnotationsArray[2] = aCircle.getCOSObject();
      tbcAnnotationsArray[3] = txtLink.getCOSObject();
      assertEquals(4, tbcAnnotationsArray.length);

      // add the annotations to the page
      pdPage = new PDPage();
      pdPage.setAnnotations(annotationsList);

      // create test output directory
      OUT_DIR.mkdirs();
   }

   /**
    * Test getting a PDModel element is in sync with underlying COSArray
    */
   @Test
   public void getFromList() throws Exception {
      COSArrayList<PDAnnotation> cosArrayList = new COSArrayList<PDAnnotation>(annotationsList, annotationsArray);

      for (int i = 0; i < cosArrayList.size(); i++) {
         PDAnnotation annot = cosArrayList.get(i);
         assertEquals("PDAnnotations cosObject at " + i + " shall be equal to index " + i + " of COSArray",
             annotationsArray.get(i), annot.getCOSObject());

         // compare with Java List/Array
         assertEquals("PDAnnotations at " + i + " shall be at index " + i + " of List",
             tbcAnnotationsList.get(i), annot);
         assertEquals("PDAnnotations cosObject at " + i + " shall be at position " + i + " of Array",
             tbcAnnotationsArray[i], annot.getCOSObject());
      }
   }

   /**
    * Test adding a PDModel element is in sync with underlying COSArray
    */
   @Test
   public void addToList() throws Exception {
      COSArrayList<PDAnnotation> cosArrayList = new COSArrayList<PDAnnotation>(annotationsList, annotationsArray);

      // add new annotation
      PDAnnotationSquareCircle aSquare = new PDAnnotationSquareCircle(PDAnnotationSquareCircle.SUB_TYPE_SQUARE);
      cosArrayList.add(aSquare);

      assertEquals("List size shall be 5", 5, annotationsList.size());
      assertEquals("COSArray size shall be 5", 5, annotationsArray.size());

      PDAnnotation annot = annotationsList.get(4);
      assertEquals("Added annotation shall be 4th entry in COSArray",
          4, annotationsArray.indexOf(annot.getCOSObject()));
      assertEquals("Provided COSArray and underlying COSArray shall be equal", annotationsArray, cosArrayList.getCOSArray());
   }

   /**
    * Test removing a PDModel element by index is in sync with underlying COSArray
    */
   @Test
   public void removeFromListByIndex() throws Exception {
      COSArrayList<PDAnnotation> cosArrayList = new COSArrayList<PDAnnotation>(annotationsList, annotationsArray);

      int positionToRemove = 2;
      PDAnnotation toBeRemoved = cosArrayList.get(positionToRemove);

      assertEquals("Remove operation shall return the removed object",toBeRemoved, cosArrayList.remove(positionToRemove));
      assertEquals("List size shall be 3", 3, cosArrayList.size());
      assertEquals("COSArray size shall be 3", 3, annotationsArray.size());

      assertEquals("PDAnnotation shall no longer exist in List",
          -1, cosArrayList.indexOf(tbcAnnotationsList.get(positionToRemove)));
      assertEquals("COSObject shall no longer exist in COSArray",
          -1, annotationsArray.indexOf(tbcAnnotationsArray[positionToRemove]));
   }

   /**
    * Test removing a unique PDModel element by index is in sync with underlying COSArray
    */
   @Test
   public void removeUniqueFromListByObject() throws Exception {
      COSArrayList<PDAnnotation> cosArrayList = new COSArrayList<PDAnnotation>(annotationsList, annotationsArray);

      int positionToRemove = 2;
      PDAnnotation toBeRemoved = annotationsList.get(positionToRemove);

      assertTrue("Remove operation shall return true",cosArrayList.remove(toBeRemoved));
      assertEquals("List size shall be 3", 3, cosArrayList.size());
      assertEquals("COSArray size shall be 3", 3, annotationsArray.size());

      // compare with Java List/Array to ensure correct object at position
      assertEquals("List object at 3 is at position 2 in COSArrayList now",
          cosArrayList.get(2), tbcAnnotationsList.get(3));
      assertEquals("COSObject of List object at 3 is at position 2 in COSArray now",
          annotationsArray.get(2), tbcAnnotationsList.get(3).getCOSObject());
      assertEquals("Array object at 3 is at position 2 in underlying COSArray now",
          annotationsArray.get(2), tbcAnnotationsArray[3]);

      assertEquals("PDAnnotation shall no longer exist in List",
          -1, cosArrayList.indexOf(tbcAnnotationsList.get(positionToRemove)));
      assertEquals("COSObject shall no longer exist in COSArray",
          -1, annotationsArray.indexOf(tbcAnnotationsArray[positionToRemove]));

      assertFalse("Remove shall not remove any object",cosArrayList.remove(toBeRemoved));
   }

   /**
    * Test removing a unique PDModel element by index is in sync with underlying COSArray
    */
   @Test
   public void removeAllUniqueFromListByObject() throws Exception {
      COSArrayList<PDAnnotation> cosArrayList = new COSArrayList<PDAnnotation>(annotationsList, annotationsArray);

      int positionToRemove = 2;
      PDAnnotation toBeRemoved = annotationsList.get(positionToRemove);

      List<PDAnnotation> toBeRemovedInstances = Collections.singletonList(toBeRemoved);

      assertTrue("Remove operation shall return true",cosArrayList.removeAll(toBeRemovedInstances));
      assertEquals("List size shall be 3", 3, cosArrayList.size());
      assertEquals("COSArray size shall be 3", 3, annotationsArray.size());

      assertFalse("Remove shall not remove any object",cosArrayList.removeAll(toBeRemovedInstances));
   }


   /**
    * Test removing a multiple appearing PDModel element by index is in sync with underlying COSArray
    */
   @Test
   public void removeMultipleFromListByObject() throws Exception {
      COSArrayList<PDAnnotation> cosArrayList = new COSArrayList<PDAnnotation>(annotationsList, annotationsArray);

      int positionToRemove = 1;
      PDAnnotation toBeRemoved = tbcAnnotationsList.get(positionToRemove);

      assertTrue("Remove operation shall return true",cosArrayList.remove(toBeRemoved));
      assertEquals("List size shall be 3", 3, cosArrayList.size());
      assertEquals("COSArray size shall be 3", 3, annotationsArray.size());

      assertTrue("Remove operation shall return true",cosArrayList.remove(toBeRemoved));
      assertEquals("List size shall be 2", 2, cosArrayList.size());
      assertEquals("COSArray size shall be 2", 2, annotationsArray.size());
   }

   /**
    * Test removing a unique PDModel element by index is in sync with underlying COSArray
    */
   @Test
   public void removeAllMultipleFromListByObject() throws Exception {
      COSArrayList<PDAnnotation> cosArrayList = new COSArrayList<PDAnnotation>(annotationsList, annotationsArray);

      int positionToRemove = 1;
      PDAnnotation toBeRemoved = annotationsList.get(positionToRemove);

      List<PDAnnotation> toBeRemovedInstances = Collections.singletonList(toBeRemoved);

      assertTrue("Remove operation shall return true",cosArrayList.removeAll(toBeRemovedInstances));
      assertEquals("List size shall be 2", 2, cosArrayList.size());
      assertEquals("COSArray size shall be 2", 2, annotationsArray.size());

      assertFalse("Remove shall not remove any object",cosArrayList.removeAll(toBeRemovedInstances));
   }

   @Test
   public void removeFromFilteredListByIndex() throws Exception
   {
      // removing from a filtered list is not permitted
      thrown.expect(UnsupportedOperationException.class);

      // retrieve all annotations from page but the link annotation
      // which is 2nd in list - see above setup
      AnnotationFilter annotsFilter = new AnnotationFilter()
      {
         @Override
         public boolean accept(PDAnnotation annotation)
         {
            return !(annotation instanceof PDAnnotationLink);
         }
      };

      COSArrayList<PDAnnotation> cosArrayList = (COSArrayList<PDAnnotation>) pdPage.getAnnotations(annotsFilter);

      // this call should fail
      cosArrayList.remove(1);
   }


   @Test
   public void removeFromFilteredListByObject() throws Exception
   {
      // removing from a filtered list is not permitted
      thrown.expect(UnsupportedOperationException.class);

      // retrieve all annotations from page but the link annotation
      // which is 2nd in list - see above setup
      AnnotationFilter annotsFilter = new AnnotationFilter()
      {
         @Override
         public boolean accept(PDAnnotation annotation)
         {
            return !(annotation instanceof PDAnnotationLink);
         }
      };

      COSArrayList<PDAnnotation> cosArrayList = (COSArrayList<PDAnnotation>) pdPage.getAnnotations(annotsFilter);

      // remove object
      int positionToRemove = 1;
      PDAnnotation toBeRemoved = cosArrayList.get(positionToRemove);

      // this call should fail
      cosArrayList.remove(toBeRemoved);

   }

   @Test
   public void removeSingleDirectObject() throws IOException {

      // generate test file
      PDDocument pdf = new PDDocument();

      PDPage page = new PDPage();
      pdf.addPage(page);

      ArrayList<PDAnnotation> pageAnnots = new ArrayList<PDAnnotation>();
      PDAnnotationTextMarkup txtMark = new PDAnnotationTextMarkup(PDAnnotationTextMarkup.SUB_TYPE_HIGHLIGHT);
      PDAnnotationLink txtLink = new PDAnnotationLink();

      // enforce the COSDictionaries to be written directly into the COSArray
      txtMark.getCOSObject().getCOSObject().setDirect(true);
      txtLink.getCOSObject().getCOSObject().setDirect(true);

      pageAnnots.add(txtMark);
      pageAnnots.add(txtMark);
      pageAnnots.add(txtMark);
      pageAnnots.add(txtLink);
      assertEquals("There shall be 4 annotations generated", 4, pageAnnots.size());

      page.setAnnotations(pageAnnots);

      pdf.save(OUT_DIR + "/removeSingleDirectObjectTest.pdf");
      pdf.close();

      pdf = PDDocument.load(new File(OUT_DIR + "/removeSingleDirectObjectTest.pdf"));
      page = pdf.getPage(0);

      COSArrayList<PDAnnotation> annotations = (COSArrayList<PDAnnotation>) page.getAnnotations();

      assertEquals("There shall be 4 annotations retrieved", 4, annotations.size());
      assertEquals("The size of the internal COSArray shall be 4", 4, annotations.getCOSArray().size());

      PDAnnotation toBeRemoved = annotations.get(0);
      annotations.remove(toBeRemoved);

      assertEquals("There shall be 3 annotations left", 3, annotations.size());
      assertEquals("The size of the internal COSArray shall be 3", 3, annotations.getCOSArray().size());

      pdf.close();
   }

   @Test
   public void removeSingleIndirectObject() throws IOException {

      // generate test file
      PDDocument pdf = new PDDocument();
      PDPage page = new PDPage();
      pdf.addPage(page);

      ArrayList<PDAnnotation> pageAnnots = new ArrayList<PDAnnotation>();
      PDAnnotationTextMarkup txtMark = new PDAnnotationTextMarkup(PDAnnotationTextMarkup.SUB_TYPE_HIGHLIGHT);
      PDAnnotationLink txtLink = new PDAnnotationLink();

      pageAnnots.add(txtMark);
      pageAnnots.add(txtMark);
      pageAnnots.add(txtMark);
      pageAnnots.add(txtLink);
      assertEquals("There shall be 4 annotations generated", 4, pageAnnots.size());

      page.setAnnotations(pageAnnots);

      pdf.save(OUT_DIR + "/removeSingleIndirectObjectTest.pdf");
      pdf.close();

      pdf = PDDocument.load(new File(OUT_DIR + "/removeSingleIndirectObjectTest.pdf"));
      page = pdf.getPage(0);

      COSArrayList<PDAnnotation> annotations = (COSArrayList<PDAnnotation>) page.getAnnotations();

      assertEquals("There shall be 4 annotations retrieved", 4, annotations.size());
      assertEquals("The size of the internal COSArray shall be 4", 4, annotations.getCOSArray().size());

      PDAnnotation toBeRemoved = annotations.get(0);

      annotations.remove(toBeRemoved);

      assertEquals("There shall be 3 annotations left", 3, annotations.size());
      assertEquals("The size of the internal COSArray shall be 2", 3, annotations.getCOSArray().size());

      pdf.close();
   }

   // @Test
   // PDFBOX-4669, PDFBOX-4723
   // This test is currently disabled with the removeAll function not properly
   // working. See the discussion in above mentioned tickets about currently not implementing equals which the 
   // functionality would need to rely on.
   public void removeDirectObject() throws IOException {

      // generate test file
      PDDocument pdf = new PDDocument();
      PDPage page = new PDPage();
      pdf.addPage(page);

      ArrayList<PDAnnotation> pageAnnots = new ArrayList<PDAnnotation>();
      PDAnnotationTextMarkup txtMark = new PDAnnotationTextMarkup(PDAnnotationTextMarkup.SUB_TYPE_HIGHLIGHT);
      PDAnnotationLink txtLink = new PDAnnotationLink();

      // enforce the COSDictionaries to be written directly into the COSArray
      txtMark.getCOSObject().getCOSObject().setDirect(true);
      txtLink.getCOSObject().getCOSObject().setDirect(true);

      pageAnnots.add(txtMark);
      pageAnnots.add(txtMark);
      pageAnnots.add(txtMark);
      pageAnnots.add(txtLink);
      assertEquals("There shall be 4 annotations generated", 4, pageAnnots.size());

      page.setAnnotations(pageAnnots);

      pdf.save(OUT_DIR + "/removeDirectObjectTest.pdf");
      pdf.close();

      pdf = PDDocument.load(new File(OUT_DIR + "/removeDirectObjectTest.pdf"));
      page = pdf.getPage(0);

      COSArrayList<PDAnnotation> annotations = (COSArrayList<PDAnnotation>) page.getAnnotations();

      assertEquals("There shall be 4 annotations retrieved", 4, annotations.size());
      assertEquals("The size of the internal COSArray shall be 4", 4, annotations.getCOSArray().size());

      ArrayList<PDAnnotation> toBeRemoved = new ArrayList<PDAnnotation>();

      toBeRemoved.add(annotations.get(0));
      annotations.removeAll(toBeRemoved);

      assertEquals("There shall be 1 annotations left", 1, annotations.size());
      assertEquals("The size of the internal COSArray shall be 1", 1, annotations.getCOSArray().size());

      pdf.close();
   }

   @Test
   public void removeIndirectObject() throws IOException {

      // generate test file
      PDDocument pdf = new PDDocument();
      PDPage page = new PDPage();
      pdf.addPage(page);

      ArrayList<PDAnnotation> pageAnnots = new ArrayList<PDAnnotation>();
      PDAnnotationTextMarkup txtMark = new PDAnnotationTextMarkup(PDAnnotationTextMarkup.SUB_TYPE_HIGHLIGHT);
      PDAnnotationLink txtLink = new PDAnnotationLink();

      pageAnnots.add(txtMark);
      pageAnnots.add(txtMark);
      pageAnnots.add(txtMark);
      pageAnnots.add(txtLink);
      assertEquals("There shall be 4 annotations generated", 4, pageAnnots.size());

      page.setAnnotations(pageAnnots);

      pdf.save(OUT_DIR + "/removeIndirectObjectTest.pdf");
      pdf.close();

      pdf = PDDocument.load(new File(OUT_DIR + "/removeIndirectObjectTest.pdf"));
      page = pdf.getPage(0);

      COSArrayList<PDAnnotation> annotations = (COSArrayList<PDAnnotation>) page.getAnnotations();

      assertEquals("There shall be 4 annotations retrieved", 4, annotations.size());
      assertEquals("The size of the internal COSArray shall be 4", 4, annotations.getCOSArray().size());

      ArrayList<PDAnnotation> toBeRemoved = new ArrayList<PDAnnotation>();
      toBeRemoved.add(annotations.get(0));

      annotations.removeAll(toBeRemoved);

      assertEquals("There shall be 1 annotations left", 1, annotations.size());
      assertEquals("The size of the internal COSArray shall be 1", 1, annotations.getCOSArray().size());

      pdf.close();
   }

   // @Test
   // PDFBOX-4669, PDFBOX-4723
   // This test is currently disabled with the retainAll function not properly
   // working. See the discussion in above mentioned tickets about currently not implementing equals which the 
   // functionality would need to rely on.
   public void retainDirectObject() throws IOException {

      // generate test file
      PDDocument pdf = new PDDocument();
      PDPage page = new PDPage();
      pdf.addPage(page);

      ArrayList<PDAnnotation> pageAnnots = new ArrayList<PDAnnotation>();
      PDAnnotationTextMarkup txtMark = new PDAnnotationTextMarkup(PDAnnotationTextMarkup.SUB_TYPE_HIGHLIGHT);
      PDAnnotationLink txtLink = new PDAnnotationLink();

      // enforce the COSDictionaries to be written directly into the COSArray
      txtMark.getCOSObject().getCOSObject().setDirect(true);
      txtLink.getCOSObject().getCOSObject().setDirect(true);

      pageAnnots.add(txtMark);
      pageAnnots.add(txtMark);
      pageAnnots.add(txtMark);
      pageAnnots.add(txtLink);
      assertEquals("There shall be 4 annotations generated", 4, pageAnnots.size());

      page.setAnnotations(pageAnnots);

      pdf.save(OUT_DIR + "/retainDirectObjectTest.pdf");
      pdf.close();

      pdf = PDDocument.load(new File(OUT_DIR + "/retainDirectObjectTest.pdf"));
      page = pdf.getPage(0);

      COSArrayList<PDAnnotation> annotations = (COSArrayList<PDAnnotation>) page.getAnnotations();

      assertEquals("There shall be 4 annotations retrieved", 4, annotations.size());
      assertEquals("The size of the internal COSArray shall be 4", 4, annotations.getCOSArray().size());

      ArrayList<PDAnnotation> toBeRetained = new ArrayList<PDAnnotation>();

      toBeRetained.add(annotations.get(0));
      annotations.retainAll(toBeRetained);

      assertEquals("There shall be 3 annotations left", 3, annotations.size());
      assertEquals("The size of the internal COSArray shall be 3", 3, annotations.getCOSArray().size());

      pdf.close();
   }

   // @Test
   // PDFBOX-4669, PDFBOX-4723
   // This test is currently disabled with the retainAll function not properly
   // working. See the discussion in above mentioned tickets about currently not implementing equals which the 
   // functionality would need to rely on.
   public void retainIndirectObject() throws IOException {

      // generate test file
      PDDocument pdf = new PDDocument();
      PDPage page = new PDPage();
      pdf.addPage(page);

      ArrayList<PDAnnotation> pageAnnots = new ArrayList<PDAnnotation>();
      PDAnnotationTextMarkup txtMark = new PDAnnotationTextMarkup(PDAnnotationTextMarkup.SUB_TYPE_HIGHLIGHT);
      PDAnnotationLink txtLink = new PDAnnotationLink();

      // enforce the COSDictionaries to be written directly into the COSArray
      txtMark.getCOSObject().getCOSObject().setDirect(true);
      txtLink.getCOSObject().getCOSObject().setDirect(true);

      pageAnnots.add(txtMark);
      pageAnnots.add(txtMark);
      pageAnnots.add(txtMark);
      pageAnnots.add(txtLink);
      assertEquals("There shall be 4 annotations generated", 4, pageAnnots.size());

      page.setAnnotations(pageAnnots);

      pdf.save(OUT_DIR + "/retainIndirectObjectTest.pdf");
      pdf.close();

      pdf = PDDocument.load(new File(OUT_DIR + "/retainIndirectObjectTest.pdf"));
      page = pdf.getPage(0);

      COSArrayList<PDAnnotation> annotations = (COSArrayList<PDAnnotation>) page.getAnnotations();

      assertEquals("There shall be 4 annotations retrieved", 4, annotations.size());
      assertEquals("The size of the internal COSArray shall be 4", 4, annotations.getCOSArray().size());

      ArrayList<PDAnnotation> toBeRetained = new ArrayList<PDAnnotation>();

      toBeRetained.add(annotations.get(0));
      annotations.retainAll(toBeRetained);

      assertEquals("There shall be 3 annotations left", 3, annotations.size());
      assertEquals("The size of the internal COSArray shall be 3", 3, annotations.getCOSArray().size());

      pdf.close();
   }
}